class Stream {
    constructor(data) {
        this.data = data;
        this.len = data.length;
        this.pos = 0;
    }

    readByte() {
        if (this.pos >= this.data.length) {
            throw new Error('Attempted to read past end of stream.');
        }
        if (this.data instanceof Uint8Array) return this.data[this.pos++];
        else return this.data.charCodeAt(this.pos++) & 0xff;
    }

    readBytes(n) {
        let bytes = [];
        for (let i = 0; i < n; i++) {
            bytes.push(this.readByte());
        }
        return bytes;
    }

    read(n) {
        let chars = '';
        for (let i = 0; i < n; i++) {
            chars += String.fromCharCode(this.readByte());
        }
        return chars;
    }

    readUnsigned() {
        // Little-endian.
        let unsigned = this.readBytes(2);
        return (unsigned[1] << 8) + unsigned[0];
    }
}

/**
 * 名称：canvas绘制gif图
 * 版本：20220920
 * 作者：李青逸
 * Q Q：1787750205
 * 使用示例：
    new canvasToGif({
        src,
        ctx,
        width: canvas.width,
        height: canvas.height
    })
 */
(function(window, document) {
    let canvasToGif = function(options) {
        this.FRAME_LIST = []; // 存放每一帧以及对应的延时
        this.TEMP_CANVAS = document.createElement('canvas'); //用来拿 imagedata 的工具人
        this.TEMP_CANVAS_CTX = null; // 工具人的 getContext('2d')
        this.GIF_INFO = {}; // GIF 的一些信息
        this.STREAM = null;
        this.LAST_DISPOSA_METHOD = null;
        this.CURRENT_FRAME_INDEX = -1; //当前帧的下标
        this.TRANSPARENCY = null;
        this.options = Object.assign(
            {
                src: '',
                ctx: null,
                canvas: null,
                width: 300,
                height: 100
            },
            options
        );
        this.loadGIF();
    };
    canvasToGif.prototype = {
        // 转流数组
        byteToBitArr: function(bite) {
            let byteArr = [];
            for (let i = 7; i >= 0; i--) {
                byteArr.push(!!(bite & (1 << i)));
            }
            return byteArr;
        },
        // Generic functions
        bitsToNum: function(ba) {
            return ba.reduce(function(s, n) {
                return s * 2 + n;
            }, 0);
        },
        lzwDecode: function(minCodeSize, data) {
            // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
            let pos = 0; // Maybe this streaming thing should be merged with the Stream?
            function readCode(size) {
                let code = 0;
                for (let i = 0; i < size; i++) {
                    if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
                        code |= 1 << i;
                    }
                    pos++;
                }
                return code;
            }

            let output = [],
                clearCode = 1 << minCodeSize,
                eoiCode = clearCode + 1,
                codeSize = minCodeSize + 1,
                dict = [];

            function clear() {
                dict = [];
                codeSize = minCodeSize + 1;
                for (let i = 0; i < clearCode; i++) {
                    dict[i] = [i];
                }
                dict[clearCode] = [];
                dict[eoiCode] = null;
            }

            let code = null,
                last = null;
            while (true) {
                last = code;
                code = readCode(codeSize);

                if (code === clearCode) {
                    clear();
                    continue;
                }
                if (code === eoiCode) {
                    break;
                }
                if (code < dict.length) {
                    if (last !== clearCode) {
                        dict.push(dict[last].concat(dict[code][0]));
                    }
                } else {
                    if (code !== dict.length) {
                        throw new Error('Invalid LZW code.');
                    }
                    dict.push(dict[last].concat(dict[last][0]));
                }
                output.push.apply(output, dict[code]);

                if (dict.length === 1 << codeSize && codeSize < 12) {
                    // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
                    codeSize++;
                }
            }
            return output;
        },
        readSubBlocks: function() {
            let size = null,
                data = '';
            do {
                size = this.STREAM.readByte();
                data += this.STREAM.read(size);
            } while (size !== 0);
            return data;
        },
        doImg: function(img) {
            if (!this.TEMP_CANVAS_CTX) {
                // 没有就创建
                this.TEMP_CANVAS_CTX = this.TEMP_CANVAS.getContext('2d');
            }
            const $this = this;
            const currIdx = this.FRAME_LIST.length,
                ct = img.lctFlag ? img.lct : this.GIF_INFO.gct;
            if (currIdx > 0) {
                // 这块不要动
                if (this.LAST_DISPOSA_METHOD === 3) {
                    if (this.CURRENT_FRAME_INDEX !== null && this.CURRENT_FRAME_INDEX > -1) {
                        this.TEMP_CANVAS_CTX.putImageData(this.FRAME_LIST[this.CURRENT_FRAME_INDEX].data, 0, 0);
                    } else {
                        this.TEMP_CANVAS_CTX.clearRect(0, 0, this.TEMP_CANVAS.width, this.TEMP_CANVAS.height);
                    }
                } else {
                    this.CURRENT_FRAME_INDEX = currIdx - 1;
                }

                if (this.LAST_DISPOSA_METHOD === 2) {
                    // Restore to background color
                    // Browser implementations historically restore to transparent; we do the same.
                    // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
                    this.TEMP_CANVAS_CTX.clearRect(0, 0, this.TEMP_CANVAS.width, this.TEMP_CANVAS.height);
                }
            }
            let imgData = this.TEMP_CANVAS_CTX.getImageData(img.leftPos, img.topPos, img.width, img.height);
            img.pixels.forEach(function(pixel, i) {
                if (pixel !== $this.TRANSPARENCY) {
                    imgData.data[i * 4 + 0] = ct[pixel][0];
                    imgData.data[i * 4 + 1] = ct[pixel][1];
                    imgData.data[i * 4 + 2] = ct[pixel][2];
                    imgData.data[i * 4 + 3] = 255; // Opaque.
                }
            });

            this.TEMP_CANVAS_CTX.putImageData(imgData, img.leftPos, img.topPos);
        },
        pushFrame: function(delay) {
            if (!this.TEMP_CANVAS_CTX) {
                return;
            }
            this.FRAME_LIST.push({
                delay,
                data: this.TEMP_CANVAS_CTX.getImageData(0, 0, this.GIF_INFO.width, this.GIF_INFO.height)
            });
        },
        // 解析
        parseExt: function(block) {
            let $this = this;
            function parseGCExt(block) {
                $this.STREAM.readByte(); // Always 4 这个必须得这样执行一次
                var bits = $this.byteToBitArr($this.STREAM.readByte());
                block.reserved = bits.splice(0, 3); // Reserved; should be 000.

                block.disposalMethod = $this.bitsToNum(bits.splice(0, 3));
                $this.LAST_DISPOSA_METHOD = block.disposalMethod;

                block.userInput = bits.shift();
                block.transparencyGiven = bits.shift();
                block.delayTime = $this.STREAM.readUnsigned();
                block.transparencyIndex = $this.STREAM.readByte();
                block.terminator = $this.STREAM.readByte();
                $this.pushFrame(block.delayTime);
                $this.TRANSPARENCY = block.transparencyGiven ? block.transparencyIndex : null;
            }

            function parseComExt(block) {
                block.comment = $this.readSubBlocks();
            }

            function parsePTExt(block) {
                // No one *ever* uses this. If you use it, deal with parsing it yourself.
                $this.STREAM.readByte(); // Always 12 这个必须得这样执行一次
                block.ptHeader = $this.STREAM.readBytes(12);
                block.ptData = $this.readSubBlocks();
            }

            function parseAppExt(block) {
                var parseNetscapeExt = function(block) {
                    $this.STREAM.readByte(); // Always 3 这个必须得这样执行一次
                    block.unknown = $this.STREAM.readByte(); // ??? Always 1? What is this?
                    block.iterations = $this.STREAM.readUnsigned();
                    block.terminator = $this.STREAM.readByte();
                };

                function parseUnknownAppExt(block) {
                    block.appData = $this.readSubBlocks();
                    // FIXME: This won't work if a handler wants to match on any identifier.
                    // handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
                }

                $this.STREAM.readByte(); // Always 11 这个必须得这样执行一次
                block.identifier = $this.STREAM.read(8);
                block.authCode = $this.STREAM.read(3);
                switch (block.identifier) {
                    case 'NETSCAPE':
                        parseNetscapeExt(block);
                        break;
                    default:
                        parseUnknownAppExt(block);
                        break;
                }
            }

            function parseUnknownExt(block) {
                block.data = $this.readSubBlocks();
            }

            block.label = this.STREAM.readByte();
            switch (block.label) {
                case 0xf9:
                    block.extType = 'gce';
                    parseGCExt(block);
                    break;
                case 0xfe:
                    block.extType = 'com';
                    parseComExt(block);
                    break;
                case 0x01:
                    block.extType = 'pte';
                    parsePTExt(block);
                    break;
                case 0xff:
                    block.extType = 'app';
                    parseAppExt(block);
                    break;
                default:
                    block.extType = 'unknown';
                    parseUnknownExt(block);
                    break;
            }
        },
        parseImg: function(img) {
            function deinterlace(pixels, width) {
                // Of course this defeats the purpose of interlacing. And it's *probably*
                // the least efficient way it's ever been implemented. But nevertheless...
                let newPixels = new Array(pixels.length);
                const rows = pixels.length / width;

                function cpRow(toRow, fromRow) {
                    const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
                    newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
                }

                // See appendix E.
                const offsets = [0, 4, 2, 1],
                    steps = [8, 8, 4, 2];

                let fromRow = 0;
                for (var pass = 0; pass < 4; pass++) {
                    for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
                        cpRow(toRow, fromRow);
                        fromRow++;
                    }
                }

                return newPixels;
            }

            img.leftPos = this.STREAM.readUnsigned();
            img.topPos = this.STREAM.readUnsigned();
            img.width = this.STREAM.readUnsigned();
            img.height = this.STREAM.readUnsigned();

            let bits = this.byteToBitArr(this.STREAM.readByte());
            img.lctFlag = bits.shift();
            img.interlaced = bits.shift();
            img.sorted = bits.shift();
            img.reserved = bits.splice(0, 2);
            img.lctSize = this.bitsToNum(bits.splice(0, 3));

            if (img.lctFlag) {
                img.lct = this.parseCT(1 << (img.lctSize + 1));
            }

            img.lzwMinCodeSize = this.STREAM.readByte();

            const lzwData = this.readSubBlocks();

            img.pixels = this.lzwDecode(img.lzwMinCodeSize, lzwData);

            if (img.interlaced) {
                // Move
                img.pixels = deinterlace(img.pixels, img.width);
            }

            this.doImg(img);
        },
        // LZW (GIF-specific)
        parseCT: function(entries) {
            // Each entry is 3 bytes, for RGB.
            let ct = [];
            for (let i = 0; i < entries; i++) {
                ct.push(this.STREAM.readBytes(3));
            }
            return ct;
        },
        parseHeader: function() {
            this.GIF_INFO.sig = this.STREAM.read(3);
            this.GIF_INFO.ver = this.STREAM.read(3);
            if (this.GIF_INFO.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.
            this.GIF_INFO.width = this.STREAM.readUnsigned();
            this.GIF_INFO.height = this.STREAM.readUnsigned();

            let bits = this.byteToBitArr(this.STREAM.readByte());
            this.GIF_INFO.gctFlag = bits.shift();
            this.GIF_INFO.colorRes = this.bitsToNum(bits.splice(0, 3));
            this.GIF_INFO.sorted = bits.shift();
            this.GIF_INFO.gctSize = this.bitsToNum(bits.splice(0, 3));

            this.GIF_INFO.bgColor = this.STREAM.readByte();
            this.GIF_INFO.pixelAspectRatio = this.STREAM.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
            if (this.GIF_INFO.gctFlag) {
                this.GIF_INFO.gct = this.parseCT(1 << (this.GIF_INFO.gctSize + 1));
            }
            // 给 this.TEMP_CANVAS 设置大小
            this.TEMP_CANVAS.width = this.GIF_INFO.width;
            this.TEMP_CANVAS.height = this.GIF_INFO.height;
            this.TEMP_CANVAS.style.width = this.GIF_INFO.width + 'px';
            this.TEMP_CANVAS.style.height = this.GIF_INFO.height + 'px';
            this.TEMP_CANVAS.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
            // console.log('this.GIF_INFO',this.GIF_INFO)
        },
        parseBlock: function() {
            let block = {};
            block.sentinel = this.STREAM.readByte();
            // console.log('block.sentinel',block.sentinel,String.fromCharCode(block.sentinel))
            switch (
                String.fromCharCode(block.sentinel) // For ease of matching
            ) {
                case '!':
                    block.type = 'ext';
                    this.parseExt(block);
                    break;
                case ',':
                    block.type = 'img';
                    this.parseImg(block);
                    break;
                case ';':
                    block.type = 'eof';
                    // 已经结束啦。结束就跑这
                    this.playGif();
                    break;
                default:
                    throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
            }

            // 递归
            if (block.type !== 'eof') {
                this.parseBlock();
            }
        },
        // 播放gif
        playGif: function(CTX, w, h) {
            let len = this.FRAME_LIST.length;
            let index = 0;
            let img = this.FRAME_LIST[index];
            let $this = this;
            // console.log('开始绘制',CTX)
            setInterval(() => {
                // console.log('单帧'+index)
                $this.TEMP_CANVAS.getContext('2d').putImageData($this.FRAME_LIST[index].data, 0, 0);
                $this.options.ctx.globalCompositeOperation = 'copy';
                if ($this.options.ctx2) {
                    $this.options.ctx2.globalCompositeOperation = 'copy';
                    $this.options.width = $this.options.width / 2;
                    $this.options.ctx.drawImage(
                        $this.TEMP_CANVAS,
                        0,
                        0,
                        $this.TEMP_CANVAS.width / 2,
                        $this.TEMP_CANVAS.height,
                        0,
                        0,
                        $this.options.canvas.width || $this.options.width,
                        $this.options.canvas.height || $this.options.height
                    );
                    $this.options.ctx2.drawImage(
                        $this.TEMP_CANVAS,
                        $this.TEMP_CANVAS.width / 2,
                        0,
                        $this.TEMP_CANVAS.width / 2,
                        $this.TEMP_CANVAS.height,
                        0,
                        0,
                        $this.options.canvas.width || $this.options.width,
                        $this.options.canvas.height || $this.options.height
                    );
                } else {
                    $this.options.ctx.drawImage(
                        $this.TEMP_CANVAS,
                        0,
                        0,
                        $this.options.canvas.width || $this.options.width,
                        $this.options.canvas.height || $this.options.height
                    );
                }
                index++;
                if (index >= len) {
                    index = 0;
                }
            }, img.delay * 10);
            console.log($this.options.id, 'canvas动图绘制完成');
        },
        loadGIF: function() {
            const h = new XMLHttpRequest();
            h.open('GET', this.options.src, true);
            // 浏览器兼容处理
            if ('overrideMimeType' in h) {
                h.overrideMimeType('text/plain; charset=x-user-defined');
            }
            // old browsers (XMLHttpRequest-compliant)
            else if ('responseType' in h) {
                h.responseType = 'arraybuffer';
            }
            // IE9 (Microsoft.XMLHTTP-compliant)
            else {
                h.setRequestHeader('Accept-Charset', 'x-user-defined');
            }
            let $this = this;
            h.onload = function(e) {
                if (this.status != 200) {
                    doLoadError('xhr - response');
                }
                // emulating response field for IE9
                if (!('response' in this)) {
                    this.response = new VBArray(this.responseText)
                        .toArray()
                        .map(String.fromCharCode)
                        .join('');
                }
                let data = this.response;
                if (data.toString().indexOf('ArrayBuffer') > 0) {
                    data = new Uint8Array(data);
                }

                $this.STREAM = new Stream(data);
                $this.parseHeader();
                $this.parseBlock();
            };

            h.onerror = function(e) {
                console.log('摆烂 error', e);
            };

            h.send();
        }
    };
    window.canvasToGif = canvasToGif;
})(window, document);

export function isAnimatedGif(src, callback) {
    var request = new XMLHttpRequest();
    request.open('GET', src, true);
    request.responseType = 'html/text';
    request.addEventListener('load', function() {
        // console.log(request.response.toString());
        if (request.response.toString().startsWith('GIF')) {
            console.log(src, '为动图');
            callback(true);
        } else {
            console.log(src, '为静态图片');
            callback(false);
        }
        // var arr = new Uint8Array(request.response),
        //     i,
        //     len,
        //     length = arr.length,
        //     frames = 0;

        // // make sure it's a gif (GIF8)
        // if (arr[0] !== 0x47 || arr[1] !== 0x49 || arr[2] !== 0x46 || arr[3] !== 0x38) {
        //     callback(false);
        //     return;
        // }
        // for (i = 0, len = length - 9; i < len, frames < 2; ++i) {
        //     if (
        //         arr[i] === 0x00 &&
        //         arr[i + 1] === 0x21 &&
        //         arr[i + 2] === 0xf9 &&
        //         arr[i + 3] === 0x04 &&
        //         arr[i + 8] === 0x00 &&
        //         (arr[i + 9] === 0x2c || arr[i + 9] === 0x21)
        //     ) {
        //         frames++;
        //     }
        // }
        // callback(frames > 1);
    });
    request.send();
}
window.isAnimatedGif = isAnimatedGif;
